package com.learning.basedao.impl;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.commons.lang.StringUtils;
import org.codehaus.plexus.util.ReflectionUtils;
import org.apache.commons.lang.Validate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcDaoSupport;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.jdbc.support.GeneratedKeyHolder;

import com.learning.basedao.BaseDao;
import com.learning.persistent.BasePersistedObject;
import com.learning.querry.Condition;


public abstract class BaseDaoImpl<T extends BasePersistedObject, PK> extends NamedParameterJdbcDaoSupport implements BaseDao<T, PK> {

    private static final Logger log = LoggerFactory.getLogger(BaseDaoImpl.class);

    /**
     * Provides the name of the table accessed by this DAO.
     */
    protected abstract String tableName();

    /**
     * Provides the schema of the table accessed by this DAO.
     */
    protected abstract String schemaName();

    /**
     * The list of attributes that we are interested in during SELECT queries.
     */
    protected abstract String[] attributes();

    protected abstract Class<T> getEntityClass();

    /**
     * Maps the values in a ResultSet to an object of type T.
     */
    protected abstract RowMapper<T> mapper();

    /**
     * Maps the object of type T to column names and values.
     */
    protected abstract Map<String,?> mapValues(T object);


//    @Override
    public PK save(T value) {
    	log.debug( "ENTER" );

        boolean exists = exists(value);

        if (exists){
            update(value);
        }
        else {
            create(value);
        }
        return (PK)value.getId();
    }

//    @Override
    public PK save(T value, Collection<String> fields) {
    	log.debug( "ENTER" );
        if (fields == null || fields.isEmpty()) {
            throw new IllegalArgumentException("List for fields to persist was null or empty.");
        }
        boolean exists = exists(value);

        if (exists){
            update(value, fields);
        }
        else {
            create(value, fields);
        }
        return (PK)value.getId();
    }

//    @Override
    public T findById(PK id) {
    	log.debug( "ENTER" );
        StringBuilder sql = new StringBuilder();

        sql.append( "SELECT ")
                .append( commaDelimitedAttributes() )
                .append( " FROM " ).append(tableName())
                .append( " WHERE id = '" ).append(id).append("'");
        
        log.info( "SQL: " + sql.toString() );

        return getJdbcTemplate().queryForObject(sql.toString(), mapper());
    }

    protected List<T> findWithAppend(String additionalSQL) {
    	log.debug( "ENTER" );
        StringBuilder sql = new StringBuilder();

        sql.append( "SELECT ")
                .append( commaDelimitedAttributes() )
                .append( " FROM " ).append(tableName())
                .append( " " ).append(additionalSQL);

        log.info( "SQL: " + sql);

        List<T> rtrn = getJdbcTemplate().query(sql.toString(), mapper());

        log.debug( "SIZE: " + rtrn.size());

        return rtrn;
    }
    
    protected List<T> findWithSQL(String additionalSQL) {
    	log.debug( "ENTER" );
      
        log.info( "SQL: " + additionalSQL);

        List<T> rtrn = getJdbcTemplate().query(additionalSQL, mapper());

        log.debug( "SIZE: " + rtrn.size());

        return rtrn;
    }
    
    protected T findObjectWithAppend(String additionalSQL) {
    	log.debug( "ENTER" );
    	List<T> listOfObjects = findWithAppend(additionalSQL);
    	if(listOfObjects == null || listOfObjects.isEmpty() ){
    		 throw new RuntimeException( "Could not find record for given search " );
    	}else if(listOfObjects.size() > 1){
    		 throw new RuntimeException( "more than one record found for given search " );
    	}else{
    		return listOfObjects.get(0);
    	}
    }

//    @Override
    public void delete(PK id) {
    	log.debug( "ENTER" );
        final String sql = "DELETE FROM " + tableName() + " WHERE id = ? ";
        final int count = getJdbcTemplate().update( sql, id );
        if( count != 1 )
            throw new RuntimeException( "Could not delete record from " + tableName() + " where id = " + id);
    }

//    @Override
    public List<T> getAll() {
    	log.debug( "ENTER" );
        StringBuilder sql = new StringBuilder();

        sql.append( "SELECT ")
                .append( commaDelimitedAttributes() )
                .append( " FROM " )
                .append(tableName());

        log.info( "SQL: " + sql );

        return getJdbcTemplate().query(sql.toString(), mapper());

    }

//    @Override
    public List<T> find(List<Condition> conditions) {
        log.debug( "ENTER" );

        Validate.notNull( conditions );

        for (Condition condition : conditions) {
            log.debug( condition.toString() );
        }

        StringBuilder sql = new StringBuilder();

        sql.append( "SELECT ")
                .append( commaDelimitedAttributes() )
                .append( " FROM " ).append(tableName())
                .append( " WHERE " );

        sql.append( build(conditions));


        log.info( "SQL: " + sql );

//		throw new RuntimeException( "STOP" );

        return getJdbcTemplate().query(sql.toString(), mapper());
    }

//    @Override
    public long count()
    {
    	log.debug( "ENTER" );
        String sql = "SELECT COUNT(1) FROM " + tableName();

        log.info( "SQL: " + sql );

        return getJdbcTemplate().queryForInt(sql);
    }

//    @Override
    public boolean exists(T value)
    {
    	log.debug( "ENTER" );
        if (value == null) {
            throw new IllegalArgumentException("Cannot retrieve count for null object.");
        }
        else if (value.getId() == null || value.getId().toString().isEmpty()) {
            return false;
        }
        else {
            // Check for primary keys that are a number
            if (Number.class.isAssignableFrom(value.getId().getClass())) {
                Number idNumber = (Number)value.getId();
                if (idNumber.floatValue() < 0) {
                    throw new IllegalStateException("Id cannot be negative.");
                }
            }
            // TODO - Address type, e.g. is the Type of String/Varchar that requires parenthesis
            StringBuilder countQueryBuilder = new StringBuilder();
            countQueryBuilder.append( "SELECT COUNT(1) FROM ");
            countQueryBuilder.append(tableName());
            countQueryBuilder.append(" WHERE ID = ?");

            int count = getJdbcTemplate().queryForInt(countQueryBuilder.toString(), value.getId());

            if (count > 1) {
                throw new IllegalStateException("There were too many rows in the database for Id " + value.getId() + " in table " + tableName());
            }
            return (count == 1);
        }
    }

    protected String commaDelimitedAttributes()
    {
    	log.debug( "ENTER" );
        StringBuilder rtrn = new StringBuilder();

        final String[] attrs = attributes();

        for (int i=0; i<attrs.length; i++) {
            rtrn.append(attrs[i]);
            if( i != attrs.length-1 )
                rtrn.append( ", " );
        }

        return rtrn.toString();
    }

    protected String build( List<Condition> conditions )
    {
    	log.debug( "ENTER" );
        StringBuilder builder = new StringBuilder();

        for(int i=0; i<conditions.size(); i++)
        {
            builder.append( conditions.get(i).toSQL() );
            if( i < conditions.size()-1 )
                builder.append( " AND ");
        }

        return builder.toString();
    }


    protected String quote( String text )
    {
        return "'" + text + "'";
    }

    public void update(T value) {
    	log.debug( "ENTER" );
        if (value == null) {
            throw new RuntimeException( "null value passed in to update entity for table " + tableName() );
        }
        else if (value.getId() == null) {
            throw new RuntimeException( "Could not update entity in table " + tableName() + ". Primary Key was null.");
        }
        final int count = performUpdate(value);

        if( count != 1 ) {
            throw new RuntimeException( "Update of entity in table " + tableName() + " failed for Primary Key " + value.getId());
        }
    }

    public void update(T value, Collection<String> fields) {
    	log.debug( "ENTER" );
        if (fields == null || fields.isEmpty()) {
            throw new IllegalArgumentException("List for fields to persist was null or empty.");
        }
        if (value == null) {
            throw new RuntimeException( "null value passed in to update entity for table " + tableName() );
        }
        else if (value.getId() == null) {
            throw new RuntimeException( "Could not update entity in table " + tableName() + ". Primary Key was null.");
        }
        final int count = performUpdate(value, fields);

        if( count != 1 ) {
            throw new RuntimeException( "Update of entity in table " + tableName() + " failed for Primary Key " + value.getId());
        }
    }

    public PK create( T value )
    {
    	log.debug( "ENTER" );
    	
        populateNewInstance(value);

        String[] tableAndSchema = tableName().split("\\.");
        String tableName = tableAndSchema[tableAndSchema.length - 1];
        	
    	Map<String,?> map = getInsertMapValues(value);
    	MapSqlParameterSource params = new MapSqlParameterSource(map);
        	
    	// convert map keys to array to ensure consistent ordering
    	String[] names = map.keySet().toArray(new String[map.size()]);
        String columnsCsv = StringUtils.join(map.keySet(), ", ");
        String valuesCsv = ":" + StringUtils.join(names, ", :");
        
        StringBuilder sql = new StringBuilder();
        sql.append( "INSERT INTO ").append(schemaName()).append(".").append( tableName )
        	.append( " ( ").append(columnsCsv).append(" ) ")
            .append( " VALUES ")
            .append( " ( ").append(valuesCsv).append(" ) ");
        	
        if (isAutoIncremented()) {
	        String[] keyFields = {"id"};
	    	GeneratedKeyHolder keyHolder = new GeneratedKeyHolder();
            getNamedParameterJdbcTemplate().update(sql.toString(), params, keyHolder, keyFields );
            applyGeneratedKey(value, keyHolder);
        }
        else {
            getNamedParameterJdbcTemplate().update(sql.toString(), params);
        }
            
        log.debug( "SQL: " + sql );
            
        if( value.getId() == null ) {
            throw new RuntimeException( "Could not save entity into " + tableName() );
        }
        return (PK)value.getId();
    }

    public PK create( T value, Collection<String> fields )
    {
    	log.debug( "ENTER" );
        return create(value);
    }


    public void populateNewInstance(T value) {
    	log.debug( "ENTER" );
        //To change body of implemented methods use File | Settings | File Templates.
    }

    protected int performUpdate(T value) {
    	log.debug( "ENTER" );
        StringBuilder sqlBuilder = new StringBuilder();
        sqlBuilder.append("UPDATE ");
        sqlBuilder.append(tableName());
        sqlBuilder.append(" SET ");
        Map<String,?> nameValueMap = mapValues(value);
        boolean isFirst = true;
        Set<String> keySet = nameValueMap.keySet();
        for (String key : keySet) {
            if ("ID".equalsIgnoreCase(key)) {
                continue;
            }
            if (isFirst) {
                isFirst = false;
                sqlBuilder.append(" ");
            }
            else {
                sqlBuilder.append(" ,");
            }
            sqlBuilder.append(key);
            sqlBuilder.append(" = :");
            sqlBuilder.append(key);
        }
        sqlBuilder.append(" WHERE ID = :id");

        String sqlString = sqlBuilder.toString();
        log.debug("***************************************************************************");
        log.debug("Update SQL : " + sqlString);
        log.debug("***************************************************************************");
        NamedParameterJdbcTemplate jt = new NamedParameterJdbcTemplate(getJdbcTemplate().getDataSource());
        return jt.update(sqlBuilder.toString(),nameValueMap);
    }

    protected int performUpdate(T value, Collection<String> fields) {
    	log.debug( "ENTER" );
        StringBuilder sqlBuilder = new StringBuilder();
        sqlBuilder.append("UPDATE ");
        sqlBuilder.append(tableName());
        sqlBuilder.append(" SET ");
        Map<String,?> nameValueMap = mapValues(value);
        boolean isFirst = true;
        Set<String> keySet = nameValueMap.keySet();
        for (String key : keySet) {
            if ("ID".equalsIgnoreCase(key)) {
                continue;
            }
            if ( ! fields.contains(key)) {
                continue;
            }
            if (isFirst) {
                isFirst = false;
                sqlBuilder.append(" ");
            }
            else {
                sqlBuilder.append(" ,");
            }
            sqlBuilder.append(key);
            sqlBuilder.append(" = :");
            sqlBuilder.append(key);
        }
        sqlBuilder.append(" WHERE ID = :id");

        String sqlString = sqlBuilder.toString();
        log.debug("***************************************************************************");
        log.debug("Update SQL : " + sqlString);
        log.debug("***************************************************************************");
        NamedParameterJdbcTemplate jt = new NamedParameterJdbcTemplate(getJdbcTemplate().getDataSource());
        return jt.update(sqlBuilder.toString(),nameValueMap);
    }

    protected String[] getInsertAttributes() {
    	log.debug( "ENTER" );
        String[] attrs = attributes();
        if (attrs == null || attrs.length <= 1) {
            throw new IllegalStateException("There are not any attributes defined for " + this.getClass().getName() + " for Database Table " + tableName());
        }
        List<String> insertAttrList = new ArrayList<String>();
        for (String attr : attrs) {
            if ("id".equalsIgnoreCase(attr)) {
                continue;
            }
            insertAttrList.add(attr);
        }
        return insertAttrList.toArray(new String[insertAttrList.size()]);
    }

    protected Map<String, ? > getInsertMapValues(T value) {
        Map<String, ?> rawMap = mapValues(value);
        if (isAutoIncremented()) {
	        rawMap.remove("id");
        }
        return rawMap;
    }

    protected boolean persistId(T value) {
        try {
            Method method = value.getClass().getMethod("getId",new Class[]{});
            return (! Number.class.isAssignableFrom(method.getReturnType()));
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
            return true;
        }
    }
    

    protected String getFullyQualifiedAttributes() {
        StringBuilder attrBuilder = new StringBuilder();
        boolean isFirst = true;
        for (String attr : attributes()) {
            if (isFirst) {
                isFirst = false;
            } else {
                attrBuilder.append(", ");
            }
            attrBuilder.append(tableName()).append(".").append(attr);
        }
        log.debug("Fully Qualified attributes : " + attrBuilder.toString());
        return attrBuilder.toString();
    }
    
//    @Override
    public void softDelete(PK id) {
    	log.debug( "ENTER" );
        final String sql = "UPDATE " + tableName() + " SET DELETED = 1 WHERE id = ? ";
        final int count = getJdbcTemplate().update( sql, id );
        if( count != 1 ) {
            throw new RuntimeException( "Could not delete record from " + tableName() + " where id = " + id);
        }
    }
    
    /**
     * The generated key produced by the JdbcTemplate is some form of java.lang.Number 
     * but the Entity class' "id" field could be a Long or an Integer, etc.  The generated key 
     * from the JdbcTemplate has been observed to be, e.g. BigDecimal, so targeted conversions
     * are applied here based on the type of the "id" field in the entity.
     * 
     * Alternative (but higher impact) solutions might be to create an abstract setId(Number) 
     * method or applyId(Number) method which the concrete classes must implement, and do their own 
     * casting/conversion as needed. 
     * 
     * @param value
     * @param keyHolder
     */
    @SuppressWarnings("unchecked")
	private void applyGeneratedKey(T value, GeneratedKeyHolder keyHolder) {
    	Number keyValue = keyHolder.getKey();
    	if (keyValue == null) {
    		value.setId(null);
    		return;
    	}
    	
    	Field field = ReflectionUtils.getFieldByNameIncludingSuperclasses("id", getEntityClass());
    	if (field.getType().equals(Short.class)) {
	    	value.setId(keyValue.shortValue());
    	}
    	if (field.getType().equals(Integer.class)) {
	    	value.setId(keyValue.intValue());
    	}
    	else if (field.getType().equals(Long.class)) {
	    	value.setId(keyValue.longValue());
    	}
    	else if (field.getType().equals(Double.class)) {
	    	value.setId(keyValue.doubleValue());
    	}
    	else if (field.getType().equals(BigDecimal.class)) {
	    	value.setId(keyValue.doubleValue());
    	}
    	else if (field.getType().equals(String.class)) {
	    	value.setId(keyValue.toString());
    	}
    	else {
    		value.setId(null);
    	}
    }
    
}
